Skip to content
lab components / Forms and input

Select

Select allows the user to choose one or more items from a list. The selected item can represent a value in a form or be used to filter or sort existing content.

This is a Lab component!

That means it doesn't satisfy our definition of done and may be changed or even deleted. For an exact status, please reach out to the Fancy team through the dev_fancy or ux_fancy channels.

import { Select } from "@siteimprove/fancylab";

#Similar components with different purposes

To clarify the differences between Select and ActionMenu, a short description is provided.

Component nameUsage
SelectChoose one or more values from a list.
Action MenuA list of individual actions that the user can perform and links.

#Examples

#Single Select

A single Select allows the user to choose only one item from a list of mutually exclusive options. If you have more than 7 items, you should use a Select. If you have 3-6 items, you can use either Radios or a Select.

A default Select consists of a button, a down-arrow icon, a list box, and a clear all button that is only displayed when an option is selected.

  • Label: when using a standalone Select, you should always have a label. A label text should inform the user what to select from a list of options. Use Form Element Wrapper around a Select to get the proper label.
  • Button: the user can click the button to show a list of items. After selection, the item is displayed in the button.
  • Down-arrow icon: indicates if the list box is shown or not.
  • List box: contains a list of selectable items that can be grouped into categories and contain an icon, description, or both (see Items Variants below). If you select an item or click outside the list box, it will be hidden.
  • Clear all button: the user can remove selected items by clicking the X icon.
const [selectedOption, setSelectedOption] = useState<string | undefined>(); return ( <Select aria-label="List of fruits" items={[ { title: "Apple", value: "apple" }, { title: "Banana", value: "banana" }, { title: "Blueberry", value: "blueberry" }, { title: "Cherry", value: "cherry" }, ]} value={selectedOption} onChange={setSelectedOption} /> );

#Multi Select

A multi Select allows the user to select or deselect one or more items.

A multi Select includes all of the elements from a single Select with the following additions:

  • Badge: shows the number of selected items.
  • Checkmark icon: indicates which item is currently selected in the list.
  • Footer: the user can confirm the selection or click the “Cancel” button to reset the selection.

Use array-like values to enable the multi select mode.

const [selectedOptions, setSelectedOptions] = useState<string[]>([]); return ( <Select aria-label="List of fruits" items={[ { title: "Apple", value: "apple" }, { title: "Banana", value: "banana" }, { title: "Blueberry", value: "blueberry" }, { title: "Cherry", value: "cherry" }, ]} value={selectedOptions} onChange={setSelectedOptions} /> );

#Searchable

The search field allows the user to type a keyword to search for an option. As the user types, the searchable Select filters the results. This helps the user find something quickly in a large list of options. For example, a searchable Select is most often used for filtering a list of countries, as it is extensive, but also easy to answer.

Use a searchable Select when

  • the user needs to filter a large list of items.
  • the user can quickly find a known option.

For developers

You can use the searchable to control whether or not to display a search bar above the list box. If set to auto (the default), the search bar will be visible only when there are more than 7 items. If set to always, it will be visible no matter the total amount of options. And, if set to never, it will not be displayed.

By default, the search bar will search for the title property of the options. However, you can specify custom searchable content to search for by using the searchableContent prop. This prop should be a function that takes an option as an argument and returns a string that will be used to match the search query. You can see it in action in the Groups example, where the searchable content takes both the option title and description.

For more complex use cases, you can use the onSearch property which is a function that takes a query string as an argument. In this case, the developer is responsible for using this query string to fetch new items and update the options list. Also, the developer must manage when to include the selected options in the list. You can see it in action in the Async examples.

const [selectedOption, setSelectedOption] = useState<string | undefined>(); return ( <Select aria-label="List of fruits" items={[ { title: "Apple", value: "apple" }, { title: "Banana", value: "banana" }, { title: "Blueberry", value: "blueberry" }, { title: "Cherry", value: "cherry" }, ]} value={selectedOption} onChange={setSelectedOption} searchable="always" /> );

#Common Use Cases

#With default option

Whenever possible, give the user the optimal option by default. This will help you avoid cases where the alternative options can often lead to errors or be confusing for inexperienced users.

You can specify an option to be selected by default using the defaultOption property. If this property is not provided or is undefined, the first option will be selected by default in single select mode.

const [selectedOption, setSelectedOption] = useState<string | undefined>(); return ( <Select aria-label="List of options" items={[ { title: "All", value: "all" }, { title: "Option 1", value: "option-1" }, { title: "Option 2", value: "option-2" }, ]} value={selectedOption} onChange={setSelectedOption} defaultOption="all" /> );

#Without default option

You can use the noDefaultOption property to not select an option by default. In this case, when the selection is empty, the placeholder will be displayed in the Selectbutton content.

For the Select without default option, a label should be provided to the user. A label should clearly describe the purpose of the selection.

Placeholder text

A placeholder text can serve as a hint, description, or an example of the information required for a particular field.

However, placeholder text should not be used as a label, since it disappears when an item is selected. In most cases, the user will forget what information belongs in a field. As a result, the user with visual and cognitive impairments are faced with an additional burden.

const [selectedOption, setSelectedOption] = useState<number | undefined>(); return ( <Select aria-label="List of options" placeholder="Choose one" items={[ { title: "Option 1", value: 1 }, { title: "Option 2", value: 2 }, ]} value={selectedOption} onChange={setSelectedOption} noDefaultOption /> );

With custom keys

In simple terms, the select list box functions as an array of options. As explained in the React documentation, each item in the list requires a key prop, which must be unique. Without this key, the component defaults to using the option's title, which can cause issues if titles aren't unique. If ensuring title uniqueness isn't feasible, it's crucial to assign a unique key to each option, as shown in the example below.

const [selectedOption, setSelectedOption] = useState<number | undefined>(); return ( <Select aria-label="List of fruits" items={[ { title: "Apple", description: "Red color", value: 1, key: "red-apple", }, { title: "Apple", description: "Green color", value: 2, key: "green-apple", }, ]} value={selectedOption} onChange={setSelectedOption} /> );

#With TypeScript union

type Fruits = "apple" | "banana" | "blueberry" | "cherry"; const [selectedOption, setSelectedOption] = useState<Fruits | undefined>(); return ( <Select aria-label="List of fruits" placeholder="Choose a fruit" items={[ { title: "Apple", value: "apple" }, { title: "Banana", value: "banana" }, { title: "Blueberry", value: "blueberry" }, { title: "Cherry", value: "cherry" }, ]} value={selectedOption} onChange={setSelectedOption} noDefaultOption /> );

#Bulk Actions

When many options need to be selected, it is recommended to include "Select all" and "Deselect all" buttons upfront to provide flexibility to the user and speed up the bulk selection process.

The following examples show the number of selected/deselected items:

  • Select all (2): indicates that two options in the list were not selected. The user can select the remaining two selectable items at once.
  • Deselect all (3): indicates that three options are already selected in the list. The user can deselect all three selected options at once.

Note the "Select all" button is disabled once all items are selected. The "Deselect all" button is only available if the user has at least one item selected. Otherwise, the button is disabled.

const [selectedOptions, setSelectedOptions] = useState<string[]>([]); return ( <Select aria-label="List of fruits" bulkActions items={[ { title: "Apple", value: "apple" }, { title: "Banana", value: "banana" }, { title: "Blueberry", value: "blueberry" }, { title: "Cherry", value: "cherry" }, ]} value={selectedOptions} onChange={setSelectedOptions} /> );

#Max Number of Items

A badge and hint text indicate the number of items that can be selected in advance. Providing helpful constraints avoids user errors. For example, the user can immediately select the correct number of items instead of receiving an error message after submitting the form.

Note that after selecting the maximum number of items, the other items are disabled. The user must deselect one of the selected items to be able to select another item.

const items = [ { title: "Apple", value: "apple" }, { title: "Banana", value: "banana" }, { title: "Blueberry", value: "blueberry" }, { title: "Cherry", value: "cherry" }, ]; const [selectedOptions, setSelectedOptions] = useState<string[]>([]); return ( <Select aria-label="List of fruits" items={items} value={selectedOptions} onChange={setSelectedOptions} maxNumberOfItems={2} /> );

#Creatable

Use creatable Select to allow the user to add a new item to the list for selection. You must also provide the onCreate function that will be called when the user clicks the create button. This function takes the value present on the search field as an argument and must return a new option (type of OptionItem) to be added to the list. The new option will be selected automatically after creating it. You can also change the label of the create button by using thecreateButtonLabel prop.

const [items, setItems] = useState(["Apple", "Banana", "Blueberry", "Cherry"]); const [selectedOption, setSelectedOption] = useState<string[]>([]); const itemize = (value: string): OptionItem<string> => ({ title: value, value }); return ( <Select items={items.map(itemize)} value={selectedOption} onChange={setSelectedOption} creatable onCreate={(newValue) => { setItems([...items, newValue].sort((a, b) => a.localeCompare(b))); return itemize(newValue); }} /> );

#With validation

By default, the list box will be hidden after creating a new option in single select mode and kept open in multi select mode. However, you can control this behavior by returning a boolean in the onCreate function, where false means closing the list box. This is useful when you want to validate the new option before adding it to the list. Here is an example of how to do it:

Pick or create a fruit

const [items, setItems] = useState(["Apple", "Banana", "Blueberry", "Cherry"]); const [isValid, setIsValid] = useState(true); const [selectedOption, setSelectedOption] = useState<string[]>([]); const itemize = (value: string): OptionItem<string> => ({ title: value, value }); const validate = (value: string) => { return value.length > 0 && value.length < 10; }; return ( <FormElementWrapper label="Fruit" name="Fruit" invalid={!isValid} error="Value must be between 1 and 10 characters" helptext="Pick or create a fruit" > <Select aria-label="List of fruits" creatable items={items.map(itemize)} value={selectedOption} onChange={(newValue) => { setSelectedOption(newValue); setIsValid(true); }} onCreate={(value: string) => { const trimmedValue = value.trim(); const isValidValue = validate(trimmedValue); if (!isValidValue) { setIsValid(false); return false; } setIsValid(true); setItems([...items, trimmedValue].sort((a, b) => a.localeCompare(b))); return itemize(trimmedValue); }} /> </FormElementWrapper> );

#Item variants

A selectable item can be provided with an icon and description to give additional context or instructions about what a user should select. Keep items concise and consistent.

  • Title only: the text describes the selectable item. Long items where the text is divided into multiple lines are not recommended.
  • Title and description: provide additional context for the item, no longer than one line.
  • Title, description, and icon: helps the user immediately understand the item. An icon must convey the meaning of the item and should not be used for decorative purposes.

State variations

Each item has the following states:

  • Default: the default state is the start or end state, indicating whether theSelect button is empty or filled.
  • Hover: when a user hovers over an item, a highlight (light gray background) appears indicating that the item is selectable.
  • Focus: a blue outline appears around the text area when the user tabs or uses the keyboard to navigate through the items.
  • Active/Pressed: a light blue background appears while the item is clicked.
  • Selected: the left border and checkmark icon indicate which item in the list is currently selected.
  • Disabled: the disabled option should be used infrequently. It indicates that the option is present, but is not currently available to the user.
const [selectedOption, setSelectedOption] = useState<string | undefined>(); return ( <Select aria-label="List of fruits" items={[ { title: "Apple", value: "apple-1" }, { title: "Apple disabled", value: "apple-2", disabled: true }, { title: "Apple with description", description: "Round and usually red or green.", value: "apple-3", }, { title: "Apple with icon", value: "apple-4", icon: ( <Icon> <IconLink /> </Icon> ), }, { title: "Apple with icon and description", description: "Round and usually red or green.", value: "apple-5", icon: ( <Icon> <IconLink /> </Icon> ), }, { title: "Apple with icon and description disabled", value: "apple-6", description: "Round and usually red or green.", icon: ( <Icon> <IconLink /> </Icon> ), disabled: true, }, ]} value={selectedOption} onChange={setSelectedOption} /> );

#Groups

Group related items in the list to give the user an overview of the list. Empathize with the user. Seeing a list of ungrouped options, can make it hard for the user to find the option they are looking for. Avoid having only one group in a list box.

const [selectedOption, setSelectedOption] = useState<string[]>([]); return ( <Select aria-label="List of fruits" searchable="always" searchableContent={(item) => item.title + " " + item.description} items={[ { heading: "Fruits", items: [ { title: "Apple", description: "Round and usually red or green.", value: "apple", }, { title: "Banana", description: "Oblong thing that starts out green and becomes yellow, then black.", value: "banana", }, { title: "Blueberry", description: "It’s a berry and it’s blue.", value: "blueberry", }, { title: "Cherry", description: "Like a blueberry, but larger and red.", value: "cherry", }, ], }, { heading: "Vegetables", items: [ { title: "Aspargus", description: "Easily recognizable for its long pointy spears.", value: "aspargus", }, { title: "Carrot", description: "Orange in color, but purple, black, red, white, and yellow also exist.", value: "carrot", }, { title: "Cocumber", description: "Usually considered a vegetable, however it's botanically a fruit.", value: "cocumber", }, ], }, ]} value={selectedOption} onChange={setSelectedOption} /> );

#Async

Use async Select to load asynchronous data from a remote source, either while loading or each time the value changes.

For the examples below, consider the following API:

type Fruit = { id: number; name: string; }; type API = { fruits: Fruit[]; }; const api: API = { fruits: [ { id: 0, name: "Apple" }, { id: 1, name: "Banana" }, //... { id: 14, name: "Watermelon" }, ], };

The entire options list can be fetched asynchronously and stored in a state. You can use the loading property to display a spinner in the list box indicating that options are being loaded.

type Fruit = { id: number; name: string }; const [loading, setLoading] = useState<boolean>(false); const [items, setItems] = useState<OptionItem<Fruit>[]>([]); const [selectedOption, setSelectedOption] = useState<Fruit | undefined>(api.fruits[1]); async function getFruits() { await sleep(5000); return [...api.fruits]; } function itemize(data: Fruit[]): OptionItem<Fruit>[] { return data.map((fruit) => ({ title: fruit.name, value: fruit, })); } useEffect(() => { let isSubscribed = true; setLoading(true); getFruits() .then((fruits) => isSubscribed && setItems(itemize(fruits))) .finally(() => isSubscribed && setLoading(false)); return () => { isSubscribed = false; }; }, []); return ( <Select loading={loading} items={items} value={selectedOption} onChange={setSelectedOption} compareFn={(a, b) => a?.id === b?.id} /> );

You can also use the onSearch property to fetch new options when the user types in the search bar. You must update the options list with the query results. Be sure to include the selected options in the options list when the search query is empty, otherwise the selection will not be displayed.

type Fruit = { id: number; name: string }; let [items, setItems] = useState<OptionItem<Fruit>[]>([]); const [selectedOptions, setSelectedOptions] = useState<Fruit[]>([api.fruits[1]]); const [loading, setLoading] = useState<boolean>(false); const [query, setQuery] = useState<string>(""); function itemize(data: Fruit[]): OptionItem<typeof selectedOptions>[] { return data.map((fruit) => ({ title: fruit.name, value: fruit, })); } function mergeItemsWithSelectedOptions(items: OptionItem<typeof selectedOptions>[]) { // merge the list of items with selected options, removing duplicates const selectedItems = itemize(selectedOptions); return [...items, ...selectedItems].filter( (item, index, self) => self.findIndex((i) => i.value.id === item.value.id) === index ); } function search<T>(items: T[], mapFn: (x: T) => string, query: string, caseSensitive = false) { const q = caseSensitive ? query : query.toLowerCase(); const map = caseSensitive ? mapFn : (x: T) => mapFn(x).toLowerCase(); return items.filter((item) => map(item).includes(q)); } async function onSearch(searchQuery: string, caseSensitive: boolean) { let fetchedItems: OptionItem<typeof selectedOptions>[] = []; if (searchQuery.length === 0) { const newItems = itemize(api.fruits.slice(0, 5)); fetchedItems = mergeItemsWithSelectedOptions(newItems); } else { await sleep(3000); const queryedItems = search(api.fruits, (f) => f.name, searchQuery, caseSensitive); fetchedItems = itemize(queryedItems); } setItems(fetchedItems); setLoading(false); } async function onCreate(value: string) { setLoading(true); await sleep(3000); const newFruit = { id: api.fruits.length, name: value }; api.fruits.push(newFruit); setLoading(false); return itemize([newFruit])[0]; } const debouncedSearch = useDebounceFn(onSearch, 500); useEffect(() => { debouncedSearch(query, false); }, [query]); if (query && loading) { items = search(items, (i) => i.title, query); } return ( <Select aria-label="List of fruits" bulkActions creatable onChange={setSelectedOptions} onCreate={onCreate} onSearch={(q) => { setLoading(true); setQuery(q); }} loading={loading} searchHelpText="Start typing to view and select matching items." items={items} value={selectedOptions} /> );

#Select Overview

Use the showSelectionOverview property to show a list of selected items just below the Select button. The user can easily see all selected items in the list box. In addition, the user can easily deselect items directly from the Selection Overview by clicking the X icon.

Selected items
  • Fruits
    • Round and usually red or green.
  • Vegetables
    • Easily recognizable for its long pointy spears.
const items = [ { heading: "Fruits", items: [ { title: "Apple", description: "Round and usually red or green.", value: "apple", icon: ( <Icon> <IconLink /> </Icon> ), }, { title: "Banana", description: "Oblong thing that starts out green and becomes yellow, then black.", value: "banana", }, { title: "Blueberry", description: "It’s a berry and it’s blue.", value: "blueberry", }, { title: "Cherry", description: "Like a blueberry, but larger and red.", value: "cherry", }, ], }, { heading: "Vegetables", items: [ { title: "Aspargus", description: "Easily recognizable for its long pointy spears.", value: "aspargus", }, { title: "Carrot", description: "Orange in color, but purple, black, red, white, and yellow also exist.", value: "carrot", }, { title: "Cocumber", description: "Usually considered a vegetable, however it's botanically a fruit.", value: "cocumber", }, ], }, ]; const [selectedOptions, setSelectedOptions] = useState<string[]>(["apple", "aspargus"]); return ( <Select aria-label="List of fruits and vegetables" showSelectionOverview items={items} value={selectedOptions} onChange={setSelectedOptions} /> );

#Invalid

A Select can be marked as having an error to indicate that an entered value is invalid. Use the error message to inform the user what has happened, and then provide guidance on next steps or possible solutions. The best practice is to verify the user's data before they have filled in all the fields of a form.

The error message can also indicate that the input is empty, e.g. "Select a country". This can happen if a user clicks the Submit button before they have fully entered the value. However, if we have correctly marked required or optional fields, the user knows which fields are required (see Best Practices). Follow the writing guideline for Issues and issue descriptions.

Some error message to let the user know what's wrong

const [selectedOption, setSelectedOption] = useState<string | undefined>(); return ( <FormElementWrapper label="Fruit" name="Fruit" invalid error="Some error message to let the user know what's wrong" > <Select items={[ { title: "Apple", value: "apple" }, { title: "Banana", value: "banana" }, { title: "Blueberry", value: "blueberry" }, { title: "Cherry", value: "cherry" }, ]} onChange={setSelectedOption} value={selectedOption} /> </FormElementWrapper> );

#Disabled

The disabled state indicates that the Select exists but is not available under some circumstances. This can be used to maintain continuity of the layout and to communicate that it may be available later. If possible, provide a hint text or a visual clue to explain why the Select is disabled to avoid user confusion.


Selected items
const [selectedOption1, setSelectedOption1] = useState<string | undefined>(); const [selectedOption2, setSelectedOption2] = useState<string[]>(["apple", "banana"]); return ( <> <Select aria-label="List of fruits" items={[ { title: "Apple", value: "apple" }, { title: "Banana", value: "banana" }, { title: "Blueberry", value: "blueberry" }, { title: "Cherry", value: "cherry" }, ]} onChange={setSelectedOption1} value={selectedOption1} disabled /> <br /> <Select aria-label="List of fruits" items={[ { title: "Apple", value: "apple" }, { title: "Banana", value: "banana" }, { title: "Blueberry", value: "blueberry" }, { title: "Cherry", value: "cherry" }, ]} onChange={setSelectedOption2} value={selectedOption2} showSelectionOverview disabled /> </> );

#Custom option element

If the item variants do not meet your requirements, you can use custom option elements. Consistency is critical when using custom option elements. Make sure they have a consistent pattern and contain only the necessary information. This example shows the consistency of labels with basic checkboxes. Note that you can provide custom renderers both for the options in the list box and for the selected items in the selection overview.

Selected items
  • Remove Apple from cart
  • Remove Banana from cart
const [selectedOptions, setSelectedOptions] = useState<string[]>(["apple", "banana"]); return ( <Select aria-label="List of fruits" listboxHeading="Shopping list" items={[ { title: "Apple", value: "apple" }, { title: "Banana", value: "banana" }, { title: "Blueberry", value: "blueberry" }, { title: "Cherry", value: "cherry" }, ]} value={selectedOptions} onChange={setSelectedOptions} optionRenderer={(props) => { const { item, onSelect, isSelected } = props; return ( <BaseSelectOption key={item.title} style={{ boxShadow: "none" }} {...props}> <Checkbox tabIndex={-1} disabled={item.disabled} onChange={() => onSelect(item)} checked={isSelected} value={item.title} > {item.title} </Checkbox> </BaseSelectOption> ); }} showSelectionOverview overviewOptionRenderer={(props) => { const { item } = props; return ( <BaseOverviewOption key={item.title} {...props}> <InlineText> Remove <strong>{item.title}</strong> from cart </InlineText> </BaseOverviewOption> ); }} /> );

#Custom option title

You can also use a simpler way to create custom option elements. This example shows the use of the useOptionTitleRenderer hook to customize the way the option title is rendered.

Selected items
const [selectedOptions, setSelectedOptions] = useState<string[]>(["apple"]); const customRenderers = useOptionTitleRenderer<string[]>((title, opt) => ( <Pill variant={{ type: "static" }} size={opt.isOverview ? "medium" : "small"}> <InlineText tone={opt.disabled ? "subtle" : "neutralDark"}> <TextHighlight value={title} needle={opt.needle} caseSensitive={opt.caseSensitive} /> </InlineText> </Pill> )); return ( <Select aria-label="List of fruits" items={[ { title: "Apple", value: "apple" }, { title: "Banana", value: "banana" }, { title: "Blueberry", value: "blueberry", disabled: true }, { title: "Cherry", value: "cherry" }, ]} value={selectedOptions} onChange={setSelectedOptions} showSelectionOverview searchable="always" {...customRenderers} /> );

#Custom button element

If the default button does not meet your requirements, you can use a custom button element. Consistency is critical when using a custom button element. Make sure it contain only the necessary information. This example shows a label with description.

const [selectedOption, setSelectedOption] = useState<string | undefined>("blueberry"); return ( <Select aria-label="List of fruits" buttonRenderer={(option?: OptionItem<string>) => option ? ( <BigSmall style={{ textAlign: "start", maxWidth: "80%" }} big={option.title} small={ <span style={{ overflow: "hidden", textOverflow: "ellipsis", whiteSpace: "nowrap", display: "block", }} > {option.description} </span> } /> ) : ( "Select a fruit" ) } buttonProps={{ size: "large" }} items={[ { title: "Apple", description: "Round and usually red or green.", value: "apple", }, { title: "Banana", description: "Oblong thing that starts out green and becomes yellow, then black.", value: "banana", }, { title: "Blueberry", description: "It’s a berry and it’s blue.", value: "blueberry", }, { title: "Cherry", description: "Like a blueberry, but larger and red.", value: "cherry", }, ]} value={selectedOption} onChange={setSelectedOption} noDefaultOption /> );

#Custom size

Choose an appropriate width

The items should not be wider than the text. The text may be truncated if it's wider than the item, making it hard for users to read.

Custom size support provides more flexibility in structuring layouts and should be used to create a hierarchy of importance within the page. Use a consistent width when used alongside other Form Element Wrapper on the same page. Use the fullWidth size sparingly.

To control the width of the button, you can use

  • the width property to adjust the width of the button.
  • the fullWidth property to fully extend the width of the button.

By default, the list box has a minimum width of 15 rem, and a maximum width of 37.5 rem. If you want to control the width of the list box, it is recommend to use

  • the listboxWidth property to adjust the width of the list box.
  • the noListboxMinWidth property to disable the default minimum list box width.
  • the noListboxMaxWidth property to disable the default maximum list box width.

Adjust height for list box

By default, the list box has a maximum height of 20.25 rem, which is about 7.5 items in the list. This means that 7 items will completely fit in the list box and the 8th item will be cut in half, which alongside the scrollbar, visually indicates that there are more items in the list box.

When using custom option elements, the number of items displayed can vary, but make sure the user can clearly see these indicators of list box continuity, especially in a Modal.

Fixed width



Fixed width for button and list box



Full width



Full width with selection overview

Selected items
  • Round and usually red or green.
  • It’s a berry and it’s blue.
const [selectedOption, setSelectedOption] = useState<string[]>(["apple", "blueberry"]); const commomProps = { items: [ { title: "Apple", description: "Round and usually red or green.", value: "apple", }, { title: "Banana", description: "Oblong thing that starts out green and becomes yellow, then black.", value: "banana", }, { title: "Blueberry", description: "It’s a berry and it’s blue.", value: "blueberry", }, { title: "Cherry", description: "Like a blueberry, but larger and red.", value: "cherry", }, ], searchable: "always" as const, bulkActions: true, value: selectedOption, onChange: setSelectedOption, ariaLabel: "List of fruits", }; return ( <> <Paragraph>Fixed width</Paragraph> <Select {...commomProps} width={600} /> <br /> <br /> <Paragraph>Fixed width for button and list box</Paragraph> <Select {...commomProps} width={600} listboxWidth={600} /> <br /> <br /> <Paragraph>Full width</Paragraph> <Select {...commomProps} fullWidth /> <br /> <br /> <Paragraph>Full width with selection overview</Paragraph> <Select {...commomProps} fullWidth showSelectionOverview /> </> );

#List box placement

Sometimes, the list box may not be visible because it is placed outside the viewport. You can use the placement property to specify where the list box should be placed. The default value is bottom-start.

const [showModal, setShowModal] = useState(false); const [selectedOptions, setSelectedOptions] = useState<number[]>([]); return ( <> <Button onClick={() => setShowModal(true)}>Trigger modal</Button> <Modal shown={showModal} headerTitle="Placement Example" onClose={() => setShowModal(false)}> <Modal.Content> <FormElementWrapper label="Fruit" name="Fruit"> <Select aria-label="List of fruits" bulkActions placement="auto-start" items={[ { value: 0, title: "Apple" }, { value: 1, title: "Banana" }, { value: 2, title: "Blueberry" }, { value: 3, title: "Cherry" }, { value: 4, title: "Grape" }, { value: 5, title: "Guava" }, { value: 6, title: "Lemon" }, { value: 7, title: "Lime" }, { value: 8, title: "Orange" }, { value: 9, title: "Peach" }, { value: 10, title: "Pear" }, { value: 11, title: "Pineapple" }, { value: 12, title: "Raspberry" }, { value: 13, title: "Strawberry" }, { value: 14, title: "Watermelon" }, ]} value={selectedOptions} onChange={setSelectedOptions} /> </FormElementWrapper> </Modal.Content> </Modal> </> );

#Usage with data-observe-keys

Use data-observe-key on the main component and/or individual items to create identifiers, which are useful for tracking user interactivity, for example. Inner buttons and inputs, such as confirm, cancel, clear, bulk actions, and search bar, will be assigned with the same data-observe-key plus a discriminator. For instance, when using <Select data-observe-key="foo" />, the clear button will be assigned with foo-ClearButton. See below a complete example and note this pattern in action.

Selected items
const [items, setItems] = useState(["Apple", "Banana", "Blueberry", "Cherry"]); const [selectedOptions, setSelectedOptions] = useState<string[]>(["banana"]); const itemize = (value: string): OptionItem<string> => ({ title: value, value: value.toLowerCase(), "data-observe-key": `${value}Item`, }); return ( <Select bulkActions showSelectionOverview data-observe-key="FruitSelect" aria-label="List of fruits" items={items.map(itemize)} value={selectedOptions} onChange={setSelectedOptions} creatable onCreate={(newValue) => { setItems([...items, newValue]); return itemize(newValue); }} /> );

#Indeterminate State

The useSelectIndeterminateState hook enhances the Select component by providing a subset of props with a custom state manager. This enables the creation of an intermediate selection state. Let's take a look at an example that demonstrates how to utilize this hook to introduce a new type of selection: partial selection. In this scenario, each option represents a tag, and each tag can be active on zero or multiple websites. A tag is considered fully selected if it is active on all websites. If it is active on some, but not all websites, it is classified as partially selected. Finally, if a tag is not active on any website, it is considered unselected.

Selected items
  • 2 of 6 sites are tagged
  • 4 of 6 sites are tagged
  • 6 of 6 sites are tagged
type Tag = { name: string; sitesCount: number }; // maximum number of sites const totalOfSites = 6; // this state simulates the tags list from your API const [tagsFromAPI, setTagsFromAPI] = useState<Tag[]>([ { name: "Tag 1", sitesCount: 0 }, { name: "Tag 2", sitesCount: 2 }, { name: "Tag 3", sitesCount: 4 }, { name: "Tag 4", sitesCount: 6 }, ]); // local copy of the tags list that will be updated with new sites count const [tags, setTags] = useState<Tag[]>(tagsFromAPI); const [selectedTags, setSelectedTags] = useState<Tag[]>(tags.filter((tag) => tag.sitesCount > 0)); const onChange = (newSelection: Tag[], updatedTags: Tag[]) => { // update the local state setSelectedTags(newSelection); // submit the changes to your API to update the tag sites count console.log("Call your API to update all tag sites count", updatedTags); setTagsFromAPI(updatedTags); }; const compareFn = (a: Tag, b: Tag) => a.name === b.name; const itemize = (tag: Tag): OptionItem<Tag> => ({ title: tag.name, value: tag, description: `${tag.sitesCount} of ${totalOfSites} sites are tagged`, }); const indeterminateStateProps = useSelectIndeterminateState<Tag[]>( tagsFromAPI, [tags, setTags], "sitesCount", totalOfSites, onChange, compareFn ); return ( <Select {...indeterminateStateProps} aria-label="List of tags on site" items={tags.map(itemize)} value={selectedTags} showSelectedItemsOnTop={false} showSelectionOverview bulkActions creatable onCreate={(value: string) => { const newTag = { name: value, sitesCount: totalOfSites }; setTags([...tags, newTag]); return itemize(newTag); }} /> );

#Properties

PropertyDescriptionDefinedValue
valueRequired
unknownValue of the form control
onChangeRequired
functionCallback for onChange event
nameOptional
stringName applied to the form control
idOptional
stringId applied to the form control
invalidOptional
booleanIs the form control invalid
onBlurOptional
functionCallback for onBlur event
aria-labelOptional
stringLabel of the form control
aria-describedbyOptional
stringID of an an element that describes what the form control is for
aria-labelledbyOptional
stringID of an an element that labels this form control
itemsOptional
object[]List of options or groups of options
compareFnOptional
functionFunction that return true is two items are equal
placeholderOptional
stringPlaceholder for the select
iconOnlyOptional
booleanPopover anchor becomes an icon-only button
fullWidthOptional
booleanShould the select be full width?
defaultOptionOptional
unknownOption selected by default. If undefined, the default is the first option. Use noDefaultOption property to disable it.
noDefaultOptionOptional
booleanIf true, does not use the default option.
disabledOptional
booleanCan the select button be clicked
loadingOptional
booleanDisplays a spinner in the listbox
searchableOptional
"always" | "auto" | "never"Enables searching functionality. If set to auto, search bar is visible only when there are more than 7 items.
onSearchOptional
functionHandler for searching event
caseSensitiveOptional
booleanEnables case sensitive search.
searchPlaceholderOptional
stringPlaceholder for search field
searchHelpTextOptional
elementPlaceholder for search field
searchLabelOptional
stringAria-label for search field
searchableContentOptional
functionDefines which text content the search should be applied to. Defaults to the options title.
bulkActionsOptional
booleanEnables bulk actions such as select/deselect all
creatableOptional
booleanEnables creating options when the search doesn't exactly match any options
onCreateOptional
functionCallback for onCreate option event
createButtonLabelOptional
stringLabel to be displayed on the create option button
hideClearButtonOptional
booleanHides the button to clear the selection
showSelectedItemsOnTopOptional
booleanIf enabled, shows selected items on top when opening the listbox. In multi-selection mode, it defaults to true.
showSelectionOverviewOptional
booleanShows selected items in a box bellow the select input
overviewLabelOptional
stringLabel that describes the overview list
roleOptional
"listbox" | "menu"Role of the list box
keyboardTypingIntervalOptional
numberAmount of time in milliseconds used to identify that the user has finished typing a string
maxNumberOfItemsOptional
numberMaximum number of items that can be selected
optionRendererOptional
functionCustom renderer for options
overviewOptionRendererOptional
functionCustom renderer for overview options
buttonPropsOptional
objectprops to pass down to the button
buttonRefOptional
objectRef of the button
widthOptional
numberControls the width of the select button.
listboxWidthOptional
numberControls the width of the select listbox.
noListboxMinWidthOptional
booleanIf true, the default listbox minimum width won't be set
noListboxMaxWidthOptional
booleanIf true, the default listbox maximum width won't be set
placementOptional
"auto" | "auto-end" | "auto-start" | "bottom" | "bottom-end" | "bottom-start" | "left" | "left-end" | "left-start" | "right" | "right-end" | "right-start" | "top" | "top-end" | "top-start"Preferred placement for the listbox
allowedAutoPlacementsOptional
literal-union[]Allowed placements for the listbox when using an "auto" value for the "placement" prop
hideChevronOptional
booleanHide the chevron icon
buttonRendererOptional
functionDefines how the selected item is rendered in the button. Defaults to the option title.
listboxHeadingOptional
elementHeading for the listbox
stateManagerBuilderOptional
functionCustom function to handle selection changes, where the input is a list of options affected and the output is a list of selected options
data-observe-keyOptional
stringUnique string, used by external script e.g. for event tracking
classNameOptional
stringCustom className that's applied to the outermost element (only intended for special cases)
styleOptional
objectStyle object to apply custom inline styles (only intended for special cases)
tabIndexOptional
numberTab index of the outermost HTML element of the component
onKeyDownOptional
functionCallback for onKeyDown event
onMouseDownOptional
functionCallback for onMouseDown event
onMouseEnterOptional
functionCallback for onMouseEnter event
onMouseLeaveOptional
functionCallback for onMouseLeave event
onFocusOptional
functionCallback for onFocus event

#Guidelines

#Best practices

#General

Use Select when:

the user can select one or multiple items from a list of options, preferably between 7 to 15 items.

#Placement

A Select is typically used in the following places:

#Style

  • Keep the label and description concise. Multi-line text wrapping is discouraged. Consider revising the text or using an alternative UI component that gives your content more space.
  • The Select component should be vertically aligned with the grid and other Form Element Wrapper components on a page.

#Required or optional

  • Currently, all fields in a form are assumed to be mandatory, and optional fields are marked. Therefore, it is recommended to provide a hint text that either states "All fields are required" or "All fields are required unless marked as optional".
  • Optional fields are marked with the text "(optional)" at the end of the label.
  • Asterisks (*) should never be used to indicate that a field is optional.

#Order of options

  • The order of the list should be based on frequency of use, if possible.
  • In forms, a different order, such as alphabetical, may be more appropriate, such as a list of country names.

#Do not use when

  • there are only 1-2 items. Choose a component that meets your needs.
  • the user is very familiar with the data, e.g. the day, month, or year of birth. Searching for these options in a long list box is tedious and can add to the user's workload. Instead, use the Input Field.
  • the user can trigger an action based on the selected option, such as displaying a modal or dynamic controls. Instead, use the Action Menu.
  • the user can select only one option from a list of fewer than 7 mutually exclusive options. In this case, use a Radios instead.
  • displaying information that is too complex. Keep the selection of items as simple as possible.

#Accessibility

For designers

  • Do not rely on placeholder text to explain input fields - combine a concise label with supportive help text.
  • Label the input field with the information it should contain. Labels should be positioned outside the field so that they are always visible.
  • A long list of options should be used with caution. If there are many choices, the user must scroll, making it impossible to see all of them at once. Longer and narrower lists require more time for the user to move the mouse pointer between them.
  • The user should not be navigated to another page, a modal, or changed to a different view without first being asked for confirmation.

For developers

A Select must convey both its visible label and its selected item by its "Accessible Name". The "Accessible Name" is usually created with aria-label or aria-labelledby. You can use the Accessibility panel in Chrome's DevTools and Safari’s Audit to inspect the "Accessible Name" of your Select.

Explore detailed guidelines for this component: Accessibility Specifications

#Writing

  • If there is no logical default option, use a {verb + noun} combination that clearly describes the selection purpose as the default text for the placeholder, such as "Select tag(s)".
  • Keep labels short and concise, aim for 1-3 words.
  • Labels, placeholder text, and items should be in sentence case.
  • Do not include punctuation after a label.
  • Avoid unnecessary words and articles such as "the", "an", or "a".

#Notable Changes

#Version 52.0.0

The multiple prop and both SingleSelectProps and MultiSelectProps types were removed in favor of a single SelectProps type. With this change, the type inference logic has been improved to determine the mode of the Select based on the value prop's type. If the valueprop is an array, then the multi select mode is enabled, otherwise, the single select mode takes place. These changes streamline the codebase and make it easier to use the Select component.

Therefore, to adapt to these changes, it is necessary to remove the multiple prop and explicit literals in the React code and use the new common SelectProps type.

#Before

const [value, setValue] = useState<string[]>([]); <Select<string> multiple value={value} onChange={setValue} />

#After

const [value, setValue] = useState<string[]>([]); <Select value={value} onChange={setValue} />

#Version 60.0.0

The onCreate function must now return an OptionItem or a boolean. Also, developers don't need to include the newly created option in the list of selected options, as the component will automatically select it.

#Before

const [items, setItems] = useState<string[]>(["Foo", "Bar"]); const [selectedOption, setSelectedOption] = useState<string>(); const itemize = (value: string): OptionItem<string> => ({ title: value, value }); <Select items={items.map(itemize)} value={selectedOption} onChange={setSelectedOption} creatable onCreate={(newValue) => { setItems([...items, newValue]); setSelectedOption(newValue); }} />

#After

const [items, setItems] = useState<string[]>(["Foo", "Bar"]); const [selectedOption, setSelectedOption] = useState<string>(); const itemize = (value: string): OptionItem<string> => ({ title: value, value }); <Select items={items.map(itemize)} value={selectedOption} onChange={setSelectedOption} creatable onCreate={(newValue) => { setItems([...items, newValue]); return itemize(newValue); }} />